#!/usr/bin/env python
# -*- coding: UTF-8 -*-
#
#   Copyright © 2008 Philip Xu(pyx) a.k.a 8pm (eightpm at gmail dot com)
#
#   This program is free software: you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation, either version 3 of the License, or
#   (at your option) any later version.
#   
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#   
#   You should have received a copy of the GNU General Public License
#   along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
#   ---------------8<-----------------8<------------------8<---------------
#
#   sdc - stix document converter
#         stix stands for Simple Typesetting In XML
#

# NOTE: As it turns out, this little piece of code need a python interpreter
#       with version 2.5 or later. 'Coz I used the python 2.5's ternary
#       operation syntax: X if Y else Z.
#       See PEP-308 for details. http://www.python.org/dev/peps/pep-0308/
#       Also, the python 2.6 and python 3000 are coming to town soon,
#       I don't even bother to check the python version for this.
#       Call Me Evil :p

import sys
import datetime
import xml.sax

VERSION = {
    "major"     : "0",
    "minor"     : "9",
    "release"   : "2"
    }

COPYING = """sdc version %(major)s.%(minor)s.%(release)s
  a stix document converter
Copyright (C) 2008 Philip Xu(pyx) a.k.a 8pm @ ft-cn group.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
""" % VERSION

# I didn't use new style class intentionally.

class stix2LaTeX(xml.sax.ContentHandler):
    """handler that converts a stix document into LaTeX document"""

    # Badboy, baby, characters to be taken care with.
    LaTeXSpecialChars = r"\~#$%^&_{}"

    # The standard header and footer showing a little extra information
    LaTeXHeader = ("% " + '*' * 72 + '\n' +
        "% This file was generated by sdc (stix document converter)\n" +
        "% vim: set tabstop=2 softtabstop=2 shiftwidth=2:\n")
    LaTeXFooter = ("% " + '*' * 72 + '\n' +
        "%% sdc version : %(major)s.%(minor)s.%(release)s\n" % VERSION +
        "%% timestamp   : %s\n" % datetime.datetime.now())

    # Mappings for stix tag -> LaTeX command convertion
    # key = stix tagname, value = (begin LaTeX command, end LaTeX command)
    # NOTE: The trailing '\n's and spaces in value pairs are significant.
    TagsMapping = {
            # Tags: Document Structures
            "document"      : ("%% begin document\n", "% end of document\n"),
            "intro"         : ("\\intro ", " "),
            "chapter"       : ("\\chapter{%(name)s} ", " "),
            "section"       : ("\\section{%(name)s} ", " "),
            "subsection"    : ("\\subsection{%(name)s} ", " "),
            "subsubsection" : ("\\subsubsection{%(name)s} ", " "),
            "summary"       : ("\\summary ", " "),
            # Tags: Tips Styles
            "beware"        : ("\\begin{beware} ", "\\end{beware} "),
            "suggest"       : ("\\begin{suggest} ", "\\end{suggest} "),
            "note"          : ("\\begin{note} ", "\\end{note} "),
            "tips"          : ("\\begin{tips} ", "\\end{tips} "),
            # Tags: Special Contents
            "code"          : ("\\begin{code} ", "\\end{code} "),
            "latex"         : ("\\begin{latex} ", "\\end{latex} "),
            "program"       : ("\\program{%(file)s}{%(name)s} ", " "),
            "exercise"      : ("\\begin{exercise} ", "\\end{exercise} "),
            "shortstory"    : ("\\begin{shortstory} ", "\\end{shortstory} "),
            "advanced"      : ("\\begin{advanced} ", "\\end{advanced} "),
            # Tags: Commands
            "br"            : ("$\\\\$", ""), # line break
            "clearpage"     : ("\\clearpage ", ""), # page break
            "list"          : ("\\begin{itemize} ", "\\end{itemize} "),
            "enum"          : ("\\begin{enumerate} ", "\\end{enumerate} "),
            "item"          : ("\\item ", " "),
            "include"       : ("\\input{%(file)s} ", " "),
        }

    # Those are the tags that needs to be treated verbatim.
    VerbatimTags = [
            "beware",
            "suggest",
            "note",
            "tips",
            "code",
            "latex",
        ]

    def getOutput(self):
        return "".join(self.output)

    def hasWarning(self):
        return self.__warning

    def startDocument(self):
        # I initialize internal data members here, so that we can reuse
        # the parser again and again.

        # The output buffer,
        # using list instead of string for better performance
        self.output = []

        # Internal Flag
        # It will be set if it's inside a verbatim tag, such as <code></code>.
        # I need this 'coz I have to mimic the "verbatim" environment in LaTeX
        # as best as I can.
        self.__verbatim = False

        # Internal Flag
        # Simply need this, TeX compilers distinguish the starting and
        # ending quote mark. `` for starting quote and '' for ending quote.
        # When the first " in text content is processed, it will be set to
        # False, then the second, to True again, and so on.
        # Remember, NEVER change the value of this manually.
        self.__startquote = True

        # Internal Flag
        # This one will be set to True if it found any stix syntax warning,
        # such as missing a necessary attribute, occured during parsing.
        # You can check its value outside the class with method hasWarning()
        # NOTE: It has NOTHING TO DO with the XML parser warning.
        #       It just show the well-formness of stix document in question.
        self.__warning = False

        self.output.append(stix2LaTeX.LaTeXHeader)

    def endDocument(self):
        self.output.append(stix2LaTeX.LaTeXFooter)

    def startElement(self, name, attrs):
        try:
            format_strings = stix2LaTeX.TagsMapping[name.lower()]
            try:
                self.output.append(format_strings[0] % attrs)
            except KeyError, e:
                # If it gets here, it is because some % directive in the
                # format string that requires a value, and that value is
                # not exist in attrs, most of the time. So, that's a warning.
                self.output.append(
                  "\n%% stix warning - missing attribute [%s] in tag <%s>\n" %
                  (str(e), name)
                  )
                self.__warning = True

        except KeyError, e:
            # It couldn't find the tag in TagsMapping, which means it is not
            # a valid stix tag, so, a warning is generated in the output.
            self.output.append(
                "\n%% stix warning - unknown opening tag: <%s>\n" % name
                )
            self.__warning = True
        if name.lower() in stix2LaTeX.VerbatimTags:
            self.__verbatim = True

    def endElement(self, name):
        try:
            self.output.append(stix2LaTeX.TagsMapping[name.lower()][1])
        except KeyError, e:
            # In the case of closing tag, it's a lot more easier, if it
            # got this far, that means it couldn't find the key in
            # TagsMapping, thus, an unknown tag name warning.
            self.output.append(
                "\n%% stix warning - unknown closing tag: </%s>\n" % name
                )
            self.__warning = True
        if name.lower() in stix2LaTeX.VerbatimTags:
            self.__verbatim = False

    def characters(self, content):
        if not self.__verbatim:
            # deal with the quote mark
            content = self.__toLaTeXquote(content)
            # escape all LaTeX special characters
            for ch in stix2LaTeX.LaTeXSpecialChars:
                # I figured out using '\verb' is better than escape with '\'
                # at least in this situation. Simpler coding, too. 
                content = content.replace(ch, '\\verb"' + ch +'"')

        self.output.append(content)

    # This is a helper function, it will keep track of the quote mark pairing
    # status using internal flag self.__startquote.
    def __toLaTeXquote(self, str):
        tmp = str.split('\"', 1)
        while len(tmp) == 2:            # Which means a '\"' has been found.
            if self.__startquote:       # ..then it is the first in the pair
                str = "``".join(tmp)
            else:                       # ..it is the second one in the pair
                str = "''".join(tmp)
            self.__startquote = not self.__startquote
            tmp = str.split('\"', 1)    # I had to duplicate the code since
                                        # there is no do .. while in python
        return str


class sdc:
    """stix(Simple typesetting in XML) document converter
    
    It converts well-formed stix document into other format 
    such as LaTeX, XHTML etc.
    """
    def __init__(self, handler_class):
        self.handler = handler_class()

    def feed(self, XML):
        """converts XML Document
        
        call this, then get the result in string by calling getOutput()
        you should also check if there was warning with method hasWarning()
        """
        parser = xml.sax.make_parser()
        parser.setContentHandler(self.handler)
        parser.parse(XML)

    def getOutput(self):
        return self.handler.getOutput()

    def hasWarning(self):
        return self.handler.hasWarning()

def main():
    from optparse import OptionParser

    usage = """usage: %prog [options] [source] [output]
    source - source file [default to standard input]
    output - output file [default to standard output]
    """
    parser = OptionParser(usage=usage)

    parser.add_option("--version", action="callback",
            callback=lambda o, s, v, p: sys.exit(sys.stderr.write(COPYING)),
            help="print the version number and exit.")

    (options, args) = parser.parse_args()

    if len(args) > 2:
        sys.stderr.write("too many arguments, use option --help for help\n")
        sys.exit(2)

    source_file = None
    output_file = None

    if len(args) >= 1:
        source_file = args[0]
    if len(args) == 2:
        output_file = args[1]

    try:
        source = open(source_file) if source_file else sys.stdin
        output = open(output_file, "w") if output_file else sys.stdout

        converter = sdc(stix2LaTeX)

        try:
            converter.feed(source)
        except xml.sax.SAXException, e:
            sys.exit("\nError: %s\n" % str(e))

        if converter.hasWarning():
            sys.stderr.write(
                    "Warning in stix syntax, please check the output file.\n")
        text = converter.getOutput().encode("UTF-8")
        output.write(text)

    except IOError, e:
        sys.exit("\nIOError: %s\n" % str(e))


if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        sys.exit("\nInterrupted by user.\n")

# vim: set ts=4 sts=4 sw=4 et fdm=indent:  *************** vim modeline **
